<?php
/**************************************************************************
 * Naanal PHP Framework, Simple, Efficient and Developer Friendly
 * Ver 3.0, Copyright (C) <2010>  <Tamil Amuthan. R>
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ************************************************************************/

class ClsLIPFilter
{
    private $rules_table    =""; //path to rules table
    private $code_rules     =array('allow'=>'','deny'=>'');
    private $action         ="";
    private $source         ="";
    private $ipClient       =null;
    private $force_hostname =false  ;
    private $onDenied       ='';
    private $onAllowed      ='';
    private $errors         =array();
    private $matched_rule   ="";   
	private $ruleTableExist=true; 
    
    final public function __construct($rules_table="")
	{
        $this->rules_table=$rules_table;
        if(trim($rules_table) !=""){
            if(is_file($rules_table) && is_readable($rules_table)){
                $this->loadTable($this->rules_table);
                }
            else{
                //$this->trace("WARNING: unable to load $rules_table.\n");
                $this->ruleTableExist=false;
                }    
            }
        }
    
    final public function start(){
        $this->ipClient     =($this->ipClient === null)?$this->clientIp():$this->ipClient;
        
        if( !$this->isAllowed($this->ipClient) ){
            switch($this->action){
                case 'redirect':
                    echo '<script type="text/javascript">location.href="'.$this->source.'"</script>';
                break;
                case 'show':
                    echo $this->source;
                break;
                default:
                    header('HTTP/1.1 403 Forbidden');
                break;
                }
            if($this->onDenied != '')
                call_user_func($this->onDenied,$this->ipClient,$this->matched_rule,$this->errors);
            exit();
            }
        else{
            if($this->onAllowed != '')
                call_user_func($this->onAllowed,$this->ipClient,$this->matched_rule,$this->errors);
            }
        }
    
    final private function storeRule($type,$values,$ruleStr){
        $this->ipClient =($this->ipClient === null)?$this->clientIp():$this->ipClient;
        $clientIp       =$this->ip2int($this->ipClient);
        $action         =$values[1];
        
        $code_to_eval="if(__MATCH__){
            \$rule_finded=\"{$ruleStr}\";
            return true;
            } \n";

        switch($type){
            case 'range':
                $from   =$this->ip2int($values[2]);
                $to     =$this->ip2int($values[3]);

                $tmp    =$from;
                $from   =($from < $to)  ?$from  :$to;
                $to     =($tmp < $to)   ?$to    :$tmp;
                $this->code_rules[$action]=str_replace('__MATCH__',"$clientIp >= $from  && $clientIp <= $to",$code_to_eval);
            break;
            case 'simple':
                $ip=$this->ip2int($values[2]);
                $this->code_rules[$action]=str_replace('__MATCH__',"$clientIp == $ip",$code_to_eval);
            break;
            }
        }
    

    
    final public function isAllowed($ip){
    if(!$this->ruleTableExist) return false;
        $this->matched_rule="";
        $rules=$this->code_rules;       
        // first check allowed arrays. if not results, check denied array. if not results return true;
        if(!empty($rules['allow'])) {
            if(eval($rules['allow'] . "return false;")) {
                $this->matched_rule=$rule_finded;
                return true;
                }
            }
        
        if(!empty($rules['deny'])) {
            if(eval($rules['deny'] . "return false;")){
                $this->matched_rule=$rule_finded;
                return false;
                } 
            }
        return true;
        }
    
    
    final private function ip2int($ip){
        $tmp=explode('.',$ip);
        foreach($tmp as $t_key=>$t_val) $tmp[$t_key]=str_pad($t_val,3, "0", STR_PAD_LEFT); 
        return implode('',$tmp);
        }
    
    
    final private function isValidIp($ip){
        return preg_match('/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/', $ip);
        }
    
    final private function clientIp(){
            if  (!empty($_SERVER['HTTP_CLIENT_IP']))        $ip=$_SERVER['HTTP_CLIENT_IP'];
        elseif  (!empty($_SERVER['HTTP_X_FORWARDED_FOR']))  $ip=$_SERVER['HTTP_X_FORWARDED_FOR'];
        else                                                $ip=$_SERVER['REMOTE_ADDR'];
        
        return $ip;
        }    
    
    final public function loadTable($rule_table=''){
        $this->rules_table=$rule_table;
        $rulesFile=$this->rules_table;
        
        if(is_file($rulesFile) && is_readable($rulesFile)){
            $rules=file($rulesFile,FILE_IGNORE_NEW_LINES);
            if(!$rules) $rules=array();
            
            if(!empty($rules)){                
                foreach($rules as $ruleLine){
                    $this->importRule($ruleLine);
                    }
                }
            }
        }

    final public function importRule($ruleSting){
        $ruleLine=trim($ruleSting);
        if($ruleLine !=""){
            // check syntax allow/deny n.n.n.n to n.n.n.n
            preg_match('/^(deny|allow)[\s](.*)[\s]to[\s](.*)$/i',$ruleLine,$values);

            if(!empty($values) && $this->isValidIp($values[2]) && $this->isValidIp($values[3])){
                $this->storeRule('range',$values,$ruleLine);
                return;
                }
            //check syntax allow/deny simple n.n.n.n
            preg_match('/^(deny|allow)[\s](.*)$/i',$ruleLine,$values);
            
            if(!empty($values) && !empty($values[2])){
                $checkIp=$this->isValidIp($values[2]);
                if( !(bool)$checkIp ){
                    //try to resolve ip by the given string...
                    if($this->force_hostname) 
                        $checkRemoteIp=gethostbyname($checkIp);
                    
                    $values[2]=(isset($checkRemoteIp) && (bool)$checkRemoteIp !== false )?$checkRemoteIp:$values[2];
                    }
                $this->storeRule('simple',$values,$ruleLine);
                return;
                }

            if(strpos($ruleLine,'#') !== 0) $this->trace('NOTICE: unable to parse "'.$ruleLine.'"'."\n");
            }
        }
    
    
    final public function onAllow($callback=''){
        $this->onAllowed=trim((string)$callback);
        }    

    final public function onDeny($callback=''){
        $this->onDenied=trim((string)$callback);
        }    


    //CAUTION! 
    //this feature can make your firewall slow! Depends from the necessary time to resolve the IP!
    final public function forceHostname($value=true){
        $this->force_hostname=(bool)$value;
        }
    
    final public function setAction($type,$source){
        $this->action=$type;
        $this->source=$source;
        }

    final private function trace($e){
        $this->errors[]=$e;
        }
    final public function getErrors($html=false){
        return ($html == true)?
            str_replace("\n","<br />",implode($this->errors)):
            $this->errors;
        }
    final public function matchedRule(){
        return $this->matched_rule;
        }
    }
?>